﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;

namespace Tradex.Messaging {

    /// <summary>
    /// The Port class is an end point in the Tradex messaging bus archtiecture. The Port has an address and is assigned to a Bus.
    /// The Port is used to send and receive messages. Ports contain the own message buffers, so a sending operation does not block
    /// for longer than is needed to get the opposite Bus (determined by the target Address) and then insert the message into the message
    /// queue on the target Port. This requires two locks (one on the Bus, one on the target Port).
    /// </summary>
    public sealed class Port : IDisposable {

        internal Port(Bus bus, Address address) {
            _Bus = bus;
            _Address = address;
            _Queue = new Queue();
        }

        /// <summary>
        /// This is the bus the Port is attached to.
        /// </summary>
        Bus _Bus;

        /// <summary>
        /// This is the Address of the Port at the Bus.
        /// </summary>
        Address _Address;

        /// <summary>
        /// This is the queue of the Port, used internally to store messages until they are picked up and/or processed.
        /// </summary>
        Queue _Queue;

        /// <summary>
        /// Returns the Address the port has at the Bus. This address is assigned to the Port at creation time from the Bus.
        /// </summary>
        public Address Address {
            get { return _Address; }
        }

        /// <summary>
        /// Returns the bus this port is connected to, if any.
        /// </summary>
        public Bus Bus {
            get { return _Bus; }
        }

        /// <summary>
        /// This is the public method used to inject a message into the message port, i.e. to send it. The message must be properly
        /// prepared / formatted. After using the send method, the message must not be used any further.
        /// </summary>
        /// <param name="message"></param>
        public void SendMessage(Message message) {
            Contract.Requires(message != null);
            if (message == null) {
                return;
            }
            message.Sender = _Address;
            _Bus.SendMessage(message);
        }

        /// <summary>
        /// This is the public method used to retrieve a message from the port.
        /// </summary>
        /// <param name="timeout"></param>
        /// <returns>The message, or null of the message got suppressed or no message is available.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
        public Message ReceiveMessage(ushort timeout = 0) {
            Message message = _Queue.Dequeue(timeout);
            Ping ping = message as Ping;
            if (ping != null) {
                OnPing(ping);
                return null;
            }
            return message;
        }

        /// <summary>
        /// This property can contain an action delegate that is to be executed when a message arrives. If it is set, messages can not be read
        /// (unless the action invoker method is active). When a new message arrives, a work item is queued (on the ThreadPool) which will
        /// then execute the Action invoker until there are no more items in the queue. This is typically used to point to a method, for example,
        /// in a windows form that will invoke into the window ui thread and then process all messages.
        /// </summary>
        public Action<Port, Message> Action { get; set; }

        /// <summary>
        /// This action is called when a Ping message arrives. It contains both the Ping message as well as the generated Pong
        /// message and allows the receiver to see the state of the port. Ping handling is never done through the normal action
        /// ports. Even if using normal queuiong (ReveiveMessage), a Ping message will result in the message being skipped
        /// on the ReceiveMessage (returning a null) and the PingAction being called.
        /// 
        /// The action gets ahold of the Pong delivered as response to the Ping, so it can see both timestamps.
        /// </summary>
        public Action<Port, Pong> PingAction { get; set; }

        bool _ActionQueued = false;

        void ActionHandler(object state) {
            lock (this) {
                if (Action == null) {
                    _ActionQueued = false;
                    return;
                }
            }
            lock (this) {
                while (_Queue.Count != 0) {
                    Message msg = _Queue.Dequeue();
                    Ping ping = msg as Ping;
                    if (ping != null) {
                        OnPing(ping);
                        continue;
                    }
                    Action.Invoke(this, msg);
                }
            }
            _ActionQueued = false;

        }

        /// <summary>
        /// This internal method is used by the Bus to deliver a message to this port.
        /// </summary>
        /// <param name="Message"></param>
        internal void DeliverMessage(Message Message) {
            lock (this) {
                _Queue.Enqueue(Message);
                if (Action != null && _ActionQueued == false) {
                    _ActionQueued = true;
                    System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(ActionHandler));
                }
            }
        }

        private void OnPing(Ping ping) {
            Pong pong = new Pong(ping, DateTime.UtcNow);
            _Bus.SendMessage(pong);
            if (PingAction != null) {
                PingAction(this, pong);
            }
        }

        #region IDisposable Members

        /// <inheritdoc />
        public void Dispose() {
            if (_Bus != null) {
                _Bus.UnregisterPort(this);
                _Bus = null;
            }
            if (Action != null) {
                Action = null;
            }
            if (PingAction != null) {
                PingAction = null;
            }
            GC.SuppressFinalize(this);
        }

        #endregion
    }

}
